Using NSTimer and NSRunLoop. This technique requires storing the thread and the timer references. The thread is stored because in this example, the invalidate method could is called from a different thread. It will cause retention issues if you fail to do this.

Calling (the invalidate) method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed.

This example adds the timer to the current runloop.

NSTimer Class References

//
//  GetLocationCommand.m
//  Buzzrd
//
//  Created by Brian Mancini on 6/29/14.
//  Copyright (c) 2014 Buzzrd. All rights reserved.
//

#import "GetLocationCommand.h"

@interface GetLocationCommand()

@property (strong, nonatomic) NSTimer *timeoutTimer;
@property (strong, nonatomic) NSThread *timerThread;

@end

@implementation GetLocationCommand {
    bool executing;
    bool finished;
}

- (id)init
{
    self = [super init];
    if(self) {
        self.completionNotificationName = @"getLocationComplete";
        executing = false;
        finished  = false;
    }
    return self;
}


// iOS8 support for asynchronous NSOperations
- (bool) isAsynchronous {
    return true;
}


// iOS7 support for asynchronous NSOperations
- (bool) isConcurrent {
    return true;
}


// Override for isExecuting, required by async NSOperations
- (bool) isExecuting {
    return executing;
}


// Overrivde for isFinished, required by async NSOperations
- (bool) isFinished {
    return finished;
}


// Required by async NSOperations
// This will perform KVO for isExecuting property and call main
- (void) start {
    NSLog(@"%p:GetLocationCommand:start", self);
    
    [self willChangeValueForKey:@"isExecuting"];
    [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
    executing = true;
    [self didChangeValueForKey:@"isExecuting"];
}


// Main NSOperation code
- (void) main {
    NSLog(@"%p:GetLocationCommand:main", self);
    
    // start timeout mechanism
    self.timerThread = [NSThread currentThread];
    self.timeoutTimer = [NSTimer timerWithTimeInterval:15.0 target:self selector:@selector(timeout) userInfo:nil repeats:false];
    [[NSRunLoop currentRunLoop] addTimer:self.timeoutTimer forMode:NSDefaultRunLoopMode];
    NSLog(@"  -> Scheduled timer %p", self.timeoutTimer);
    
    // add event handlers
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(locationUpdated:) name:BZLocationManagerUpdated object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(locationErrored:) name:BZLocationManagerErrored object:nil];
    
    // start the location request
    [[BZLocationManager instance] requestLocation];
    
    // wait for timeout
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}


// Fires on deallocation
- (void)dealloc {
    NSLog(@"%p:GetLocationCommand:dealloc", self);
}


// Fires when after the elapsed timeout period
- (void)timeout {
    NSLog(@"%p:GetLocationCommand:timeout", self);
    
    CLLocation *lastLocation = [[BZLocationManager instance] requestLastLocation];
    
    if(lastLocation) {
        NSLog(@"  -> Sending last location: (%f, %f)", lastLocation.coordinate.longitude, lastLocation.coordinate.latitude);
        [self sendSuccess:lastLocation];
    } else {
        NSLog(@"  -> Sending error");
        [self  sendError:[[NSError alloc] init]];
    }
    
    [self shutdownCommand];
}


// Event handler for BZLocationManager errors
- (void)locationErrored:(NSNotification *)notification {
    NSLog(@"%p:GetLocationCommand:locationErrored", self);
        
    NSError *error = notification.userInfo[BZLocationManagerErroredErrorInfoKey];
    [self sendError:error];
    [self shutdownCommand];
}


// Event handler for BZLocationManager updates
- (void)locationUpdated:(NSNotification *)notification {
    NSLog(@"%p:GetLocationCommand:locationUpdated", self);
    
    CLLocation *location = notification.userInfo[BZLocationManagerUpdatedLocationInfoKey];
    [self sendSuccess:location];
    [self shutdownCommand];
}


// Triggers an error for the NSOperation
- (void)sendError:(NSError*)error {
    self.status = kFailure;
    self.results = error;
    [self sendCompletionFailureNotification];
}


// Triggers a succses for the NSOperation
- (void)sendSuccess:(CLLocation *)location {
    self.status = kSuccess;
    self.results = location;
    [self sendCompletionNotification];
}


// Shuts down the NSOperation
- (void)shutdownCommand {
    NSLog(@"%p:GetLocationCommand:shutdownCommand", self);
    
    // Clear out observers
    [[NSNotificationCenter defaultCenter] removeObserver:self];

    // Invalidate the timeout
    [self invalidateTimeout];
    
    // Execute KVO for isExecuting and isFinished properties
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];
    
    executing = false;
    finished = true;
    
    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

// Invalidates the timer used for timeout handling
- (void)invalidateTimeout {
    NSLog(@"%p:GetLocationCommand:invalidateTimeout", self);
    
    [self performSelector:@selector(doInvalidateTimeout) onThread:self.timerThread withObject:nil waitUntilDone:true];
}

- (void)doInvalidateTimeout {
    
    // using NSTimer
    NSLog(@"  -> Invalidating timer %p", self.timeoutTimer);
    [self.timeoutTimer invalidate];
    self.timeoutTimer = nil;
}

@end