Answer depends on 'the work'. If work is something that can be safely stopped (i.e. not some I/O blocking operation) - use Backgroundworker.CancelAsync(...)
If you do have to cut hard - I'd consider using a Process, in which case the Aborting process is cleaner - and process.WaitForExit(timeout) is your friend.
Suggested TPL is great but unfortunately does not exist in .Net 3.5.
EDIT: You can use Reactive Extensions to follow Jan de Vaan's suggestion.
Here is my 'action timeout' snip - it's mainly here for others to comment on:
    public static bool WaitforExit(this Action act, int timeout)
    {
        var cts = new CancellationTokenSource();
        var task = Task.Factory.StartNew(act, cts.Token);
        if (Task.WaitAny(new[] { task }, TimeSpan.FromMilliseconds(timeout)) < 0)
        { // timeout
            cts.Cancel();
            return false;
        }
        else if (task.Exception != null)
        { // exception
            cts.Cancel();
            throw task.Exception;
        }
        return true;
    }
EDIT: Apparently this isn't exactly what OP wanted. Here's my attempt to devise a 'cancelable' socket receiver:
public static class Ext
{
    public static object RunWithTimeout<T>(Func<T,object> act, int timeout, T obj) where T : IDisposable
    {
        object result = null;
        Thread thread = new Thread(() => { 
            try { result = act(obj); }
            catch {}    // this is where we end after timeout...
        });
        thread.Start();
        if (!thread.Join(timeout))
        {
            obj.Dispose();
            thread.Join();
        }
        return result;
    }       
}
class Test
{
    public void SocketTimeout(int timeout)
    {
        using (var sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp))
        {
            Object res = Ext.RunWithTimeout(EntryPoint, timeout, sock);
        }
    }
    private object EntryPoint(Socket sock)
    {
        var buf = new byte[256];
        sock.Receive(buf);
        return buf;
    }
}